// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.gctoolkit;

import com.microsoft.gctoolkit.aggregator.Aggregation;
import com.microsoft.gctoolkit.io.DataSource;
import com.microsoft.gctoolkit.io.GCLogFile;
import com.microsoft.gctoolkit.io.RotatingGCLogFile;
import com.microsoft.gctoolkit.io.SingleGCLogFile;
import com.microsoft.gctoolkit.jvm.Diary;
import com.microsoft.gctoolkit.jvm.JavaVirtualMachine;
import com.microsoft.gctoolkit.message.DataSourceBus;
import com.microsoft.gctoolkit.message.JVMEventBus;
import com.microsoft.gctoolkit.message.DataSourceConsumer;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The primary API for analyzing Java Garbage Collection (GC) logs.
 */
public class GCToolKit {

    private static final Logger LOGGER = Logger.getLogger(GCToolKit.class.getName());

    /**
     * Load the first implementation of JavaVirtualMachine that can process
     * the supplied DataSource, GCLog in this instance.
     * @param logFile GCLogFile DataSource
     * @return JavaVirtualMachine implementation.
     */
    private JavaVirtualMachine loadJavaVirtualMachine(GCLogFile logFile) {
        return ServiceLoader.load(JavaVirtualMachine.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .filter(jvm -> jvm.accepts(logFile))
                .findFirst()
                .orElseThrow(() -> new ServiceConfigurationError("No suitable JavaVirtualMachine implementation found"));
    }

    private DataSourceBus setupDataSourceBus() {
        return ServiceLoader.load(DataSourceBus.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .findFirst()
                .orElseThrow(() -> new ServiceConfigurationError("No suitable DataSourceBus implementation found"));

    }

    private JVMEventBus setupJVMEventBus() {
        return ServiceLoader.load(JVMEventBus.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .findFirst()
                .orElseThrow(() -> new ServiceConfigurationError("No suitable JVMEventBus implementation found"));

    }

    private void registerParsers(DataSourceBus bus, Diary diary) {
        ServiceLoader.load(DataSourceConsumer.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .filter(p->p.accepts(diary))
                .forEach(parser->bus.register(parser));

    }

    private final Set<Class<? extends Aggregation>> registeredAggregations;

    /**
     * Instantiate a GCToolKit object. The same GCToolKit object can be used to analyze
     * more than one GC log. It is not necessary to create a GCToolKit object for
     * each GC log to be analyzed. Please note, however, that GCToolKit API is not
     * thread safe.
     */
    public GCToolKit() {
        // Allow for adding aggregations from source code,
        // but don't corrupt the ones loaded by the service loader
        this.registeredAggregations = new HashSet<>();
    }

    /**
     * Loads all Aggregations defined in the application module through
     * the java.util.ServiceLoader model. To register a class that
     * provides the {@link Aggregation} API, define the following
     * in {@code module-info.java}:
     * <pre>
     * import com.microsoft.gctoolkit.aggregator.Aggregation;
     * import com.microsoft.gctoolkit.sample.aggregation.HeapOccupancyAfterCollectionSummary;
     * 
     * module com.microsoft.gctoolkit.sample {
     *     ...
     *     provides Aggregation with HeapOccupancyAfterCollectionSummary;
     * }
     * </pre>
     */
    public void loadAggregationsFromServiceLoader() {
        ServiceLoader.load(Aggregation.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .map(Aggregation::getClass)
                .forEach(registeredAggregations::add);
        //Useful for debugging
        if ( Level.FINER.equals(LOGGER.getLevel()))
            registeredAggregations.forEach(a -> LOGGER.log(Level.FINER, "Registered " + a.toString()));
    }

    /**
     * Registers an {@code Aggregation} class which can be used to perform analysis
     * on {@code JVMEvent}s. GCToolKit will instantiate the Aggregation when needed.
     * <p>
     * The {@link JavaVirtualMachine#getAggregation(Class)}
     * API will return an Aggregation that was used in the log analysis. Even though
     * an Aggregation was registered, the {@code getAggregation} method will return
     * null if the Aggregation was not used in the analysis.
     *
     * @param aggregationClass the Aggregation class to register.
     * @see Aggregation
     * @see JavaVirtualMachine
     */
    public void registerAggregation(Class<? extends Aggregation> aggregationClass) {
        registeredAggregations.add(aggregationClass);
    }

    /**
     * Perform an analysis on a GC log file. The analysis will use the Aggregations
     * that were {@link #registerAggregation(Class) registered}, if appropriate for
     * the GC log file.
     *
     * @param logFile The log to analyze, typically a
     *                   {@link SingleGCLogFile} or
     *                   {@link RotatingGCLogFile}.
     * @return a representation of the state of the Java Virtual Machine resulting
     * from the analysis of the GC log file.
     */
    public JavaVirtualMachine analyze(GCLogFile logFile) throws IOException  {
        //todo: revert this to DataSource to account for non-GC log data sources once we have a use case (maybe JFR but JFR timers drift badly ATM)
        //setup message bus
        //DataSourceBus dataSourceBus = setupDataSourceBus();
        //JVMEventBus eventBus = setupJVMEventBus();
        //registerParsers(dataSourceBus,logFile.diary());
        JavaVirtualMachine javaVirtualMachine = loadJavaVirtualMachine(logFile);
        //todo: do we need reflection????
        try {
            Method analyze = javaVirtualMachine.getClass().getMethod("analyze", Set.class, DataSource.class);
            analyze.invoke(javaVirtualMachine, this.registeredAggregations, logFile);
        } catch (ReflectiveOperationException e) {
            LOGGER.log(Level.SEVERE, "Cannot invoke analyze method", e);
        }
        return javaVirtualMachine;
    }
}
